/**
 * Copyright (c) 2018-2022, NXOS Development Team
 * SPDX-License-Identifier: Apache-2.0
 * 
 * Contains: mutex for thread
 * 
 * Change Logs:
 * Date           Author            Notes
 * 2021-11-13     JasonHu           Init
 */

#include <base/mutex.h>
#include <base/sched.h>
#include <base/thread.h>
#include <base/irq.h>
#include <base/debug.h>
#define NX_LOG_NAME "mutex"
#define NX_LOG_LEVEL NX_LOG_INFO
#include <base/log.h>

#define MUTEX_MAGIC 0x10000002

NX_Error NX_MutexInit(NX_Mutex *mutex)
{
    if (mutex == NX_NULL)
    {
        return NX_EINVAL;
    }
    if (NX_SpinInit(&mutex->lock) != NX_EOK)
    {
        return NX_EPERM;
    }
    if (NX_SpinInit(&mutex->waitLock) != NX_EOK)
    {
        return NX_EPERM;
    }
    NX_ListInit(&mutex->waitList);    
    mutex->magic = MUTEX_MAGIC;
#ifdef NX_SCHED_REALTIME
    mutex->originalPriority = NX_THREAD_INVALID_PRIORITY;
    mutex->holder = NX_NULL;
#endif /* NX_SCHED_REALTIME */
    return NX_EOK;
}

NX_Error NX_MutexInitLocked(NX_Mutex *mutex)
{
    NX_Error err;
    err = NX_MutexInit(mutex);
    if (err != NX_EOK)
    {
        return err;
    }
    NX_SpinLock(&mutex->lock);
    return NX_EOK;
}

NX_Error NX_MutexTryLock(NX_Mutex *mutex)
{
    if (mutex == NX_NULL || mutex->magic != MUTEX_MAGIC)
    {
        return NX_EFAULT;
    }

    /* disable interrupt for locking per cpu */
    NX_UArch level = NX_IRQ_SaveLevel();
    /* spin lock for mutex */
    NX_Error err = NX_SpinTryLock(&mutex->lock);
    if (err == NX_EOK)
    {
        NX_Thread * self = NX_ThreadSelf();
        if (self)
        {
            NX_ASSERT(NX_ThreadPushLock(self, mutex) == NX_EOK);
#ifdef NX_SCHED_REALTIME
            /* hold lock */
            mutex->originalPriority = self->priority;
            mutex->holder = self;
#endif /* NX_SCHED_REALTIME */
        }
    }
    NX_IRQ_RestoreLevel(level);
    return err;
}

NX_Error NX_MutexLock(NX_Mutex *mutex)
{
    NX_Error err;
    if (mutex == NX_NULL || mutex->magic != MUTEX_MAGIC)
    {
        return NX_EFAULT;
    }

    do
    {
        /* spin lock for mutex */
        err = NX_MutexTryLock(mutex);
        if (err == NX_EOK)
        {
            break;
        }
        else if (err == NX_EINTR)
        {
            return err;
        }
        else
        {
            NX_UArch level;
            NX_Thread * self = NX_ThreadSelf();
#ifdef NX_SCHED_REALTIME
            /**
             * change mutex holder priority to current thread priority
             */
            if (self->priority > mutex->originalPriority)
            {
                NX_Thread *holder = mutex->holder;
                NX_ASSERT(holder != NX_NULL);
                NX_LOG_D("raise mutex holder %s/%d priority from %d to prio %d", holder->name, holder->tid, holder->priority, self->priority);
                NX_ASSERT(NX_ThreadChangePriority(holder, self->priority) == NX_EOK);
            }
#endif /* NX_SCHED_REALTIME */
            NX_LOG_D("add thread %d to mutex wait list", self->tid);
            NX_ASSERT(NX_SpinLockIRQ(&mutex->waitLock, &level) == NX_EOK);
            NX_ListAddTail(&self->blockList, &mutex->waitList);
            if (NX_ThreadBlockLockedIRQ(self, &mutex->waitLock, level) != NX_EOK) /* block self */
            {
                return NX_EINTR;
            }
        }
    } while (1);

    return NX_EOK;
}

NX_Error NX_MutexUnlock(NX_Mutex *mutex)
{
    NX_UArch level;
    NX_Thread * waiter;
    
    if (mutex == NX_NULL || mutex->magic != MUTEX_MAGIC)
    {
        return NX_EFAULT;
    }
    NX_Thread * self = NX_ThreadSelf();
    if (self)
    {
#ifdef NX_SCHED_REALTIME
        if (self != mutex->holder)
        {
            NX_LOG_E("thread %s/%d unlock mutex %p not holder!", self->name, self->tid, mutex);
            return NX_EFAULT;
        }
#endif /* NX_SCHED_REALTIME */
        NX_Mutex * lock = NX_ThreadPopLock(self);
        NX_ASSERT(lock == mutex);
#ifdef NX_SCHED_REALTIME
        /* restore priority */
        NX_Thread *holder = (NX_Thread *)mutex->holder;
        NX_ASSERT(holder != NX_NULL);
        /**
         * mutex holder priority changed
         */
        if (holder->priority != mutex->originalPriority)
        {
            NX_LOG_D("restore mutex holder %s/%d priority from %d to prio %d", holder->name, holder->tid, holder->priority, mutex->originalPriority);
            NX_ASSERT(NX_ThreadChangePriority(holder, mutex->originalPriority) == NX_EOK);
        }
#endif /* NX_SCHED_REALTIME */
    }
#ifdef NX_SCHED_REALTIME
    mutex->holder = NX_NULL;
    mutex->originalPriority = NX_THREAD_INVALID_PRIORITY;
#endif /* NX_SCHED_REALTIME */
    NX_SpinUnlock(&mutex->lock);

    /* wake up waiter */
    NX_ASSERT(NX_SpinLockIRQ(&mutex->waitLock, &level) == NX_EOK);
    
    waiter = NX_ListFirstEntryOrNULL(&mutex->waitList, NX_Thread, blockList);
    if (waiter != NX_NULL)
    {
        NX_ListDel(&waiter->blockList);
        NX_LOG_D("wakeup thread %d from mutex wait list", waiter->tid);
        NX_ThreadUnblock(waiter);
    }
    NX_SpinUnlockIRQ(&mutex->waitLock, level);
    return NX_EOK;
}

NX_Error NX_MutexLockTimeout(NX_Mutex *mutex, NX_TimeVal ms)
{
    NX_TimeVal tvStart, delta;

    if (!mutex)
    {
        return NX_EINVAL;
    }

    if (mutex->magic != MUTEX_MAGIC)
    {
        return NX_EFAULT;
    }

    if (!ms)
    {
        return NX_MutexTryLock(mutex);
    }
    tvStart = NX_ClockGetMillisecond();
    do
    {
        if (NX_MutexTryLock(mutex) == NX_EOK)
        {
            return NX_EOK;
        }
        delta = NX_ClockGetMillisecond() - tvStart;
        /**
         * TODO: do sched if delta too big.
         */
    } while (delta < ms);
    
    return NX_ETIMEOUT;
}

NX_Bool NX_MutexIsLocked(NX_Mutex *mutex)
{
    if (mutex == NX_NULL || mutex->magic != MUTEX_MAGIC)
    {
        return NX_False;
    }
    return NX_SpinIsLocked(&mutex->lock);
}
